/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.services.reports;

import com.google.inject.Inject;
import org.joda.time.DateTime;
import org.joda.time.Period;
import org.xptracker.domain.*;
import org.xptracker.persistence.PersistenceManager;
import org.xptracker.util.TimeUtil;

import java.util.*;

public class TimeSegmentReport {
  private Timeline timeline;
  private TimeSegment segment;
  private final Map<TimelineUnits, ValueConverter> converters = new HashMap<TimelineUnits, ValueConverter>();

  @Inject
  @SuppressWarnings({"UnusedDeclaration"})
  private PersistenceManager mgr;

  @Inject
  @SuppressWarnings({"UnusedDeclaration"})
  private Domain domain;

  public TimeSegmentReport() {
    converters.put(TimelineUnits.hours, new ValueConverter() {
      public double convert(StoryState change) {
        return change.getEstimate();
      }
    });

    converters.put(TimelineUnits.points, new ValueConverter() {
      public double convert(StoryState change) {
        StoryPointMapping mapping = domain.loadPointMapping(true);
        return mapping.getMap().get(change.getSize().toString());
      }
    });

    converters.put(TimelineUnits.pointsByTask, new ValueConverter() {
      public double convert(StoryState change) {
        StoryPointMapping mapping = domain.loadPointMapping(true);
        final double amount = mapping.getMap().get(change.getSize().toString());
        return amount - (amount * change.getTaskCompletionRatio());
      }
    });

    converters.put(TimelineUnits.hoursRemaining, new ValueConverter() {
      public double convert(StoryState change) {
        double estimate = change.getEstimate();
        double effort = toHours(change.getTotalEffort());
        double remaining = estimate - effort;
        return remaining >= 0 ? remaining : 0;
      }

      private double toHours(long milliseconds) {
        return TimeUtil.fromMillis(milliseconds, "hours");
      }
    });
  }

  void setTimeline(Timeline timeline) {
    this.timeline = timeline;
  }

  void setSegment(TimeSegment segment) {
    this.segment = segment;
  }

  public void execute(TimeSegmentReportRecipient recipient) {
    DateTime currentDate = new DateTime(segment.getStartDate().getTime());
    DateTime endDate = new DateTime(segment.getStopDate().getTime());

    TimelineGranularity.Period period = timeline.getGranularity().getPeriod();
    Period p = period.convertInterval(timeline.getGranularity().getInterval()).toPeriod();

    Map<String, Story> encounteredStories = new HashMap<String, Story>();

    Set<String> presentStories = new HashSet<String>();
    while (currentDate.isBefore(endDate) || currentDate.isEqual(endDate)) {
      DateTime localEnd = currentDate.plus(p);

      List<StoryState> changes = domain.listStoryStates(currentDate.toDate(), localEnd.toDate());
      cacheStories(changes, encounteredStories);

      for (StoryState change : changes) {
        boolean meetsEntry = false;
        for (Criterion c : timeline.getEntryCriteria()) {
          meetsEntry |= c.check(change);
        }

        String itemId = change.getStoryId();
        Story s = encounteredStories.get(itemId);
        boolean remains = presentStories.contains(itemId);
        if (!remains && meetsEntry && !meetsExit(change)) {
          presentStories.add(itemId);
          recipient.storyAdded(domain, currentDate.toDate(), s, convert(timeline.getUnits(), change));
        } else if (meetsExit(change) && presentStories.remove(itemId)) {
          recipient.storyCompleted(domain, currentDate.toDate(), s, convert(timeline.getUnits(), change));
        } else if (!meetsEntry && presentStories.remove(itemId)) {
          recipient.storyRemoved(domain, currentDate.toDate(), s, convert(timeline.getUnits(), change));
        } else if (remains) {
          recipient.storyRemains(domain, currentDate.toDate(), s, convert(timeline.getUnits(), change));
        }
      }

      if (localEnd.isAfter(endDate) && currentDate.isBefore(endDate)) { // If we're straddling the end point, include it.
        currentDate = endDate;
      } else {
        currentDate = localEnd;
      }
    }
  }

  private void cacheStories(List<StoryState> changes, Map<String, Story> encounteredStories) {
    Set<String> ids = new HashSet<String>();
    for(StoryState change: changes) {
      String itemId = change.getStoryId();
      if (!encounteredStories.containsKey(itemId)) {
        ids.add(itemId);
      }
    }

    for (Story s : mgr.listObjects(Story.class, ids)) {
      encounteredStories.put(s.getId(), s);
    }
  }

  private double convert(TimelineUnits units, StoryState change) {
    return converters.get(units).convert(change);
  }

  private boolean meetsExit(StoryState change) {
    boolean meetsExit = false;
    for (Criterion c : timeline.getExitCriteria()) {
      meetsExit |= c.check(change);
    }
    return meetsExit;
  }

  private interface ValueConverter {
    double convert(StoryState change);
  }
}
